package io.gitee.mayan50.autoassociate.auto.impl;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import io.gitee.mayan50.autoassociate.annotation.AutoAssociate;
import io.gitee.mayan50.autoassociate.annotation.OneToMany;
import io.gitee.mayan50.autoassociate.annotation.OneToOne;
import io.gitee.mayan50.autoassociate.auto.AutoAssociateManager;
import io.gitee.mayan50.autoassociate.util.DBUtils;
import io.gitee.mayan50.autoassociate.util.FieldUtils;
import io.gitee.mayan50.autoassociate.util.StrUtils;
import io.gitee.mayan50.autoassociate.value.AssociateFieldEntity;
import io.gitee.mayan50.autoassociate.value.AssociateFieldList;
import org.apache.ibatis.session.SqlSession;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.beans.factory.annotation.Autowired;

import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.*;
import java.util.stream.Stream;

import static io.gitee.mayan50.autoassociate.util.FieldUtils.*;

/**
 * 关联关系关联类
 */
public class AutoAssociateManagerImpl implements AutoAssociateManager {

    @Autowired
    protected ObjectFactory<SqlSession> factory;
    @Autowired(required = false)
    protected DBUtils dbUtils;

    public DBUtils getDbUtils() {
        if (dbUtils == null) throw new RuntimeException("DBUtils Bean is not injected, please configure DBUtils Bean.");
        return dbUtils;
    }

    private final boolean baseEnable;

    protected final List<String> entityPackages;

    public AutoAssociateManagerImpl() {
        this(true, new ArrayList<>());
    }

    public AutoAssociateManagerImpl(boolean baseEnable) {
        this(baseEnable, new ArrayList<>());
    }

    public AutoAssociateManagerImpl(String... entityPackages) {
        this(true, Arrays.asList(entityPackages));
    }

    public AutoAssociateManagerImpl(boolean baseEnable, String... entityPackages) {
        this(baseEnable, Arrays.asList(entityPackages));
    }

    public AutoAssociateManagerImpl(List<String> entityPackages) {
        this(true, entityPackages);
    }

    public AutoAssociateManagerImpl(boolean baseEnable, List<String> entityPackages) {
        this.baseEnable = baseEnable;
        this.entityPackages = entityPackages;
    }

    public boolean isBaseEnable() {
        return baseEnable;
    }

    public boolean isEntity(String className) {
        if (className == null) return false;
        if (entityPackages == null || entityPackages.isEmpty()) return true;
        return entityPackages.stream().anyMatch(className::contains);
    }

    public Class<?> getMapper(Class<?> entityClass) {
        Collection<Class<?>> mappers = factory.getObject().getConfiguration().getMapperRegistry().getMappers();
        for (Class<?> mapperClass : mappers) if (getClassGenericType(mapperClass) == entityClass) return mapperClass;
        return null;
    }

    public boolean isCollection(Class<?> clazz) {
        return clazz == List.class || clazz == ArrayList.class || clazz == LinkedList.class || clazz == Set.class || clazz == HashSet.class ||
                clazz == TreeSet.class;
    }

    public <E> BaseMapper<E> getByClassMapper(Class<?> defMapperClass, Class<?> columnClass) {
        Class<?> mapperClass = defMapperClass;
        if (defMapperClass == void.class) mapperClass = getMapper(columnClass);
        if (mapperClass == null) throw new RuntimeException("Unable to find the corresponding Mapper");
        return  (BaseMapper<E>) factory.getObject().getMapper(mapperClass);
    }

    public <E> String[] dealWithCondition(String[] condition, Class<?> classType, E entity) throws NoSuchFieldException, IllegalAccessException {
        if (condition == null || condition.length == 0) return null;
        String[] values = new String[condition.length];
        int i = 0;
        for (String s : condition) {
            if (s.startsWith("a:")) {
                Field field = classType.getDeclaredField(StrUtils.humpToLine(s.substring(2)));
                field.setAccessible(true);
                values[i++] = String.valueOf(field.get(entity));
            } else values[i++] = s;
        }
        return values;
    }

    private Set<String> excludeSet(AutoAssociate autoAssociate) {
        if (autoAssociate == null || autoAssociate.excludeFiled() == null || autoAssociate.excludeFiled().length == 0) return new HashSet<>();
        return new HashSet<>(Arrays.asList(autoAssociate.excludeFiled()));
    }

    private <E> BaseMapper<E> getOneToManyBaseMapper(OneToMany oneToMany, Field field) {
        Class<?> columnClass = null;
        if (!isCollection(field.getType())) throw new RuntimeException("Not a one-to-many relationship");
        columnClass = (Class<?>) getFieldGenericType(field);
        return getByClassMapper(oneToMany.mapperEntity(), columnClass);
    }

    private <E> BaseMapper<E> getOneToOneBaseMapper(OneToOne oneToOne, Field field) {
        if (isCollection(field.getType())) throw new RuntimeException("Not a one-to-one relationship");
        return getByClassMapper(oneToOne.mapperEntity(), field.getType());
    }

    private <T, E> Map<String, AssociateFieldList<E>> getFieldListMap(Collection<T> list, AutoAssociate autoAssociate) {
        if (list == null || list.isEmpty()) return null;
        Set<String> excludeSet = excludeSet(autoAssociate);
        Class<?> entityClass = list.iterator().next().getClass();
        Map<String, AssociateFieldList<E>> fieldListMap = new HashMap<>();
        for (Field field : getField(entityClass)) {
            if (excludeSet.contains(field.getName())) continue;
            OneToMany oneToMany = field.getAnnotation(OneToMany.class);
            if (oneToMany != null) fieldListMap.put(field.getName(), new AssociateFieldList<>(oneToMany, getOneToManyBaseMapper(oneToMany, field)));
            OneToOne oneToOne = field.getAnnotation(OneToOne.class);
            if (oneToOne != null) fieldListMap.put(field.getName(), new AssociateFieldList<>(oneToOne, getOneToOneBaseMapper(oneToOne, field)));
        }
        return fieldListMap;
    }

    private <T, E> void fillFieldValue(Collection<T> list, Map<String, AssociateFieldList<E>> fieldValueMap) {
        Class<?> entityClass = list.iterator().next().getClass();
        int index = 0;
        for (T t: list) {
            if (t == null) continue;
            for (Map.Entry<String, AssociateFieldList<E>> entry : fieldValueMap.entrySet()) {
                AssociateFieldList<E> fieldValue = entry.getValue();
                if (StrUtils.isNotBlock(fieldValue.sql())) continue;
                String column = fieldValue.column();
                try {
                    if (StrUtils.isNotBlock(column)) {
                        Field columnField = getColumnField(entityClass, column);
                        columnField.setAccessible(true);
                        fieldValue.put(columnField.get(t), index);
                    }
                } catch (NoSuchFieldException | IllegalAccessException e) {
                    e.printStackTrace();
                }
            }
            ++index;
        }
    }

    public <T, E> void associateList(Collection<T> list, AutoAssociate autoAssociate) {
        Map<String, AssociateFieldList<E>> fieldListMap = getFieldListMap(list, autoAssociate);
        fillFieldValue(list, fieldListMap);
        Class<?> entityClass = list.iterator().next().getClass();
        int index = 0;
        for (T t : list) {
            if (t == null) continue;
            for (Map.Entry<String, AssociateFieldList<E>> entry : fieldListMap.entrySet()) {
                AssociateFieldList<E> fieldValue = entry.getValue();
                try {
                    Field field = t.getClass().getDeclaredField(entry.getKey());
                    field.setAccessible(true);
                    String sql = fieldValue.sql();
                    if (StrUtils.isNotBlock(sql)) field.set(t, fieldValue.isOneToOne() ?
                            getDbUtils().executeQuerySqlOne(sql, (Class<E>) getFieldGenericType(field), dealWithCondition(fieldValue.condition(), entityClass, t)) :
                            getDbUtils().executeQuerySql(sql, (Class<E>) getFieldGenericType(field), dealWithCondition(fieldValue.condition(), entityClass, t)));
                    else fieldValue.setField(t, field, index);
                } catch (NoSuchFieldException | IllegalAccessException e) {
                    e.printStackTrace();
                }
            }
            ++index;
        }
    }

    public <T, E> void associateEntity(T entity, AutoAssociate autoAssociate) {
        Set<String> excludeSet = excludeSet(autoAssociate);
        Class<?> entityClass = entity.getClass();
        for (Field field : getField(entityClass)) {
            if (excludeSet.contains(field.getName())) continue;
            AssociateFieldEntity<E> fieldEntity = null;
            OneToMany oneToMany = field.getAnnotation(OneToMany.class);
            if (oneToMany != null) fieldEntity = new AssociateFieldEntity<E>(oneToMany, getOneToManyBaseMapper(oneToMany, field));
            OneToOne oneToOne = field.getAnnotation(OneToOne.class);
            if (oneToOne != null) fieldEntity = new AssociateFieldEntity<E>(oneToOne, getOneToOneBaseMapper(oneToOne, field));
            if (fieldEntity == null) continue;
            field.setAccessible(true);
            String sql = fieldEntity.sql();
            try {
                if (StrUtils.isNotBlock(sql)) field.set(entity, fieldEntity.isOneToOne() ?
                        getDbUtils().executeQuerySqlOne(sql, (Class<E>) getFieldGenericType(field), dealWithCondition(fieldEntity.condition(), entityClass, entity)) :
                        getDbUtils().executeQuerySql(sql, (Class<E>) getFieldGenericType(field), dealWithCondition(fieldEntity.condition(), entityClass, entity)));
                Field columnField = null;
                if (StrUtils.isNotBlock(fieldEntity.column())) {
                    columnField = getColumnField(entityClass, fieldEntity.column());
                    columnField.setAccessible(true);
                }
                if (columnField == null) throw new RuntimeException("Please check if column is empty or misspelled.");
                fieldEntity.setValue(columnField.get(entity)).setField(entity, field);
            } catch (IllegalAccessException | NoSuchFieldException e) {
                e.printStackTrace();
            }
        }
    }

    public <T> T autoAssociate(T entity, AutoAssociate autoAssociate) {
        associateEntity(entity, autoAssociate);
        return entity;
    }

    public <T> Collection<T> autoAssociate(Collection<T> list, AutoAssociate autoAssociate) {
        associateList(list, autoAssociate);
        return list;
    }

    public Object autoAssociateObj(Object obj, AutoAssociate autoAssociate) {
        return obj instanceof Collection ? autoAssociate((Collection<?>) obj, autoAssociate) : autoAssociate(obj, autoAssociate);
    }

}
